#include <sys/param.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <libgen.h>
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define	NONEOP		0
#define	SETOP		1		/* do a SET operation */
#define	GETOP		2		/* do a GET operation */
#define	REMOVEOP	3		/* do a REMOVE operation */
#define	LISTOP		4		/* do a LIST operation */

#define	ATTR_MAX_VALUELEN	(64*1024)	/* max length of a value */

static char *progname;

void
usage(void)
{
	fprintf(stderr,
"Usage: %s -s attrname [-V attrvalue] pathname  # set attrribute\n"
"       %s -g attrname pathname                 # get attribute\n"
"       %s -r attrname pathname                 # remove attribute\n"
"       %s -l pathname                          # list attributes\n"
"	-s without -V reads a value from stdin\n",
	    progname, progname, progname, progname);
	exit(EXIT_FAILURE);
}

static int listattrs(const char *);
static int getattr(const char *, const char *);
static int setattr(const char *, const char *, char *);
static int attrremove(const char *, const char *);

int
main(int argc, char **argv)
{
	progname = basename(argv[0]);
	int op = NONEOP;
	char *attrname = NULL;
	char *attrvalue = NULL;
	char *filename = NULL;
	int ch;
	int res = -1;

	setlocale(LC_CTYPE, "");
	setlocale(LC_MESSAGES, "");

	attrname = attrvalue = NULL;
	while ((ch = getopt(argc, argv, "ls:V:g:r:")) != -1) {
		switch (ch) {
		case 'l':
			if (op) {
				fprintf(stderr,
				    "Only one of -s, -g, -r, or -l allowed\n");
				usage();
			}
			op = LISTOP;
			break;
		case 's':
			if (op != NONEOP  && op != SETOP) {
				fprintf(stderr,
				    "Only one of -s, -g, -r, or -l allowed\n");
				usage();
			}
			op = SETOP;
			attrname = optarg;
			break;
		case 'g':
			if (op) {
				fprintf(stderr,
				    "Only one of -s, -g, -r, or -l allowed\n");
				usage();
			}
			op = GETOP;
			attrname = optarg;
			break;
		case 'r':
			if (op) {
				fprintf(stderr,
				    "Only one of -s, -g, -r, or -l allowed\n");
				usage();
			}
			op = REMOVEOP;
			attrname = optarg;
			break;
		case 'V':
			if (op != NONEOP && op != SETOP) {
				fprintf(stderr,
				    "Only one of -s, -g, -r, or -l allowed\n");
                                usage();
			}
			op = SETOP;
			attrvalue = optarg;
			break;
		default:
			fprintf(stderr, "Unrecognized option: %c\n", (char)ch);
			usage();
			break;
		}
	}
	argc -= optind;
	argv += optind;

	if (op == NONEOP) {
		usage();
	}

	switch(argc) {
	case 0:
		fprintf(stderr, "A filename to operate on is required\n");
		exit(EXIT_FAILURE);
	case 1:
		filename = *argv;
		break;
	default:
		fprintf(stderr,
		    "A filename to operate on is required only one\n");
		exit(EXIT_FAILURE);
	}

	switch (op) {
	case LISTOP:
		if ((res = listattrs(filename)) < 0)
			fprintf(stderr, "Could not list \"%s\" for %s\n",
			    attrname, filename);
		break;

	case SETOP:
		if ((res = setattr(filename, attrname, attrvalue)) < 0)
			fprintf(stderr, "Could not set \"%s\" for %s\n",
			    attrname, filename);
		break;

	case GETOP:
		if ((res = getattr(filename, attrname)) < 0)
			fprintf(stderr, "Could not get \"%s\" for %s\n",
			    attrname, filename);
		break;

	case REMOVEOP:
		if ((res = attrremove(filename, attrname)) < 0)
			fprintf(stderr, "Could not get \"%s\" for %s\n",
			    attrname, filename);
		break;

	default:
		fprintf(stderr,
		    "At least one of -s, -g, -r, or -l is required\n");
		usage();
	}

	return (res == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
}

static int
openfile(const char *filename)
{
	int fd;

	if ((fd = open64(filename, O_RDONLY)) == -1) {
		fprintf(stderr,
		    "Cannot open %s: %s\n", filename, strerror(errno));
		return (-1);
	}

	return (fd);
}

static int
openxattrdir(const char *filename)
{
	int fd, dirfd;
	if ((fd = openfile(filename)) == -1)
		return (-1);

	if ((dirfd = openat64(fd, ".", O_RDONLY | O_XATTR)) == -1) {
		close(fd);
		fprintf(stderr,
		    "Cannot open attribute directory for %s: %s\n",
		    filename, strerror(errno));
		return (-1);
	}
	close(fd);

	return (dirfd);
}

#define MODE_GET 0
#define MODE_SET 1

static int
openxattr(const char *filename, const char *attrname, int mode)
{
	int fd, xfd;

	if ((fd = openfile(filename)) < 0)
		return (-1);

	if (mode == MODE_GET)
		xfd = openat64(fd, attrname, O_RDONLY | O_XATTR);
	else
		xfd = openat64(fd, attrname,
		    O_RDWR | O_CREAT | O_TRUNC | O_XATTR, 0644);
	close(fd);

	if (xfd < 0)
		fprintf(stderr,
		    "Cannot open %s: %s\n", filename, strerror(errno));

	return (xfd);
}

static int
listattrs(const char *filename)
{
	int xfd;
	struct dirent *dir;

	if ((xfd = openxattrdir(filename)) < 0)
		return (-1);

	DIR *d = fdopendir(xfd);
	while ((dir = readdir(d)) != NULL) {
		if (strcmp(dir->d_name, ".") == 0 ||
		    strcmp(dir->d_name, "..") == 0)
			continue;
		printf("%s\n", dir->d_name);
	}
	closedir(d);
	close(xfd);
	return (0);
}

static int
getattr(const char *filename, const char *attrname)
{
	int xfd;
	struct stat xfd_stat;
	char *attrvalue = NULL;

	if ((xfd = openxattr(filename, attrname, MODE_GET)) < 0)
		return (-1);

	if (fstat(xfd, &xfd_stat) != 0) {
		fprintf(stderr, "Cannot get fstat %s: %s\n",
		    filename, strerror(errno));
		goto out;
	}

	size_t bufsize = xfd_stat.st_size;
	if ((attrvalue = malloc(bufsize)) == NULL) {
		fprintf(stderr, "Cannot allooc memory\n");
		goto out;
	}

	size_t bytesread = read(xfd, attrvalue, bufsize);
	fwrite(attrvalue, 1, bytesread, stdout);

out:
	free(attrvalue);
	close(xfd);
	return (0);
}

static int
setattr(const char *filename, const char *attrname, char *attrvalue)
{
	int xfd;
	size_t attrlen;

	if (attrvalue == NULL) {
		attrvalue = malloc(ATTR_MAX_VALUELEN);
		if (attrvalue == NULL) {
			fprintf(stderr, "Could not set \"%s\" for %s\n",
			    attrname, filename);
			return (-1);
		}
		attrlen = fread(attrvalue, 1, ATTR_MAX_VALUELEN, stdin);
	} else {
		attrlen = strlen(attrvalue);
	}

	if ((xfd = openxattr(filename, attrname, MODE_SET)) < 0)
		return (-1);

	size_t byteswritten = write(xfd, attrvalue, attrlen);
	close(xfd);
	if (byteswritten != attrlen)
		return (-1);

	return (0);
}

static int
attrremove(const char *filename, const char *attrname)
{
	int xfd;
	if ((xfd = openxattrdir(filename)) < 0)
		return (-1);

	int ret = unlinkat(xfd, attrname, 0);
	close(xfd);

	if (ret < 0) {
		fprintf(stderr, "Cannot allooc memory\n");
		return (-1);
	}
	return (0);
}
